به آیندههای Asyncio پایتون مسلط شوید. مفاهیم ناهمزمان سطح پایین، نمونههای عملی و تکنیکهای پیشرفته را برای ساخت برنامههایی قوی و با کارایی بالا بررسی کنید.
آیندههای Asyncio باز شدهاند: یک بررسی عمیق در برنامهنویسی ناهمزمان سطح پایین در پایتون
در دنیای توسعهی پایتون مدرن، نحوهی async/await
به سنگبنایی برای ساخت برنامههای با کارایی بالا و مبتنی بر I/O تبدیل شده است. این روش، یک راه تمیز و ظریف برای نوشتن کد همروند فراهم میکند که تقریباً ترتیبی به نظر میرسد. اما در زیر این شیرینی سینتکسی سطح بالا، یک مکانیزم قدرتمند و اساسی نهفته است: آیندهی Asyncio. در حالی که ممکن است هر روز با آیندههای خام تعامل نداشته باشید، درک آنها کلید تسلط واقعی بر برنامهنویسی ناهمزمان در پایتون است. این مانند یادگیری نحوهی کارکرد موتور یک ماشین است؛ شما برای رانندگی نیازی به دانستن آن ندارید، اما اگر میخواهید یک مکانیک ماهر باشید، ضروری است.
این راهنمای جامع پرده از asyncio
برخواهد داشت. ما بررسی خواهیم کرد که آیندهها چیستند، چگونه با کروتینها و وظایف متفاوت هستند، و چرا این پریمیتو سطح پایین، سنگ بنایی است که قابلیتهای ناهمزمان پایتون بر روی آن ساخته شده است. چه در حال اشکالزدایی یک شرط مسابقه پیچیده باشید، چه در حال ادغام با کتابخانههای قدیمیتر مبتنی بر callback، یا صرفاً به دنبال درک عمیقتری از async هستید، این مقاله برای شما مناسب است.
آیندهی Asyncio دقیقاً چیست؟
در هسته خود، asyncio.Future
یک شی است که نتیجهی احتمالی یک عملیات ناهمزمان را نشان میدهد. آن را به عنوان یک مکاننما، یک وعده یا رسید برای مقداری که هنوز در دسترس نیست، در نظر بگیرید. وقتی عملیاتی را شروع میکنید که زمان میبرد تا تکمیل شود (مانند یک درخواست شبکه یا یک پرس و جو پایگاه داده)، میتوانید بلافاصله یک شی Future دریافت کنید. برنامه شما میتواند به انجام کارهای دیگر ادامه دهد، و هنگامی که عملیات در نهایت به پایان رسید، نتیجه (یا یک خطا) در داخل آن شی Future قرار میگیرد.
یک قیاس مفید دنیای واقعی، سفارش قهوه در یک کافه شلوغ است. شما سفارش خود را ثبت و پرداخت میکنید و باریستا به شما یک رسید با شماره سفارش میدهد. شما هنوز قهوهی خود را ندارید، اما رسید را دارید—وعدهی یک قهوه. اکنون میتوانید به جای ایستادن بیکار در پیشخوان، به دنبال میز بگردید یا تلفن خود را بررسی کنید. وقتی قهوه شما آماده شد، شماره شما خوانده میشود و میتوانید رسید خود را برای نتیجه نهایی «بازخرید» کنید. رسید همان Future است.
ویژگیهای کلیدی یک Future عبارتند از:
- سطح پایین: آیندهها یک بلوک ساختمانی ابتداییتر در مقایسه با وظایف هستند. آنها ذاتاً نمیدانند چگونه کدی را اجرا کنند. آنها به سادگی ظروفی برای یک نتیجه هستند که بعداً تنظیم میشود.
- Awaitable: مهمترین ویژگی یک Future این است که یک شیء awaitable است. این بدان معناست که میتوانید از کلیدواژهی
await
روی آن استفاده کنید، که اجرای کروتین شما را متوقف میکند تا زمانی که Future نتیجهای داشته باشد. - Stateful: یک Future در طول چرخهی عمر خود در یکی از چند حالت مجزا وجود دارد: Pending، Cancelled یا Finished.
آیندهها در مقابل کروتینها در مقابل وظایف: روشن کردن ابهام
یکی از بزرگترین موانع برای توسعهدهندگانی که با asyncio
آشنا نیستند، درک رابطهی بین این سه مفهوم اصلی است. آنها عمیقاً به هم مرتبط هستند، اما اهداف متفاوتی را دنبال میکنند.
1. کروتینها
یک کروتین به سادگی یک تابع است که با async def
تعریف شده است. وقتی یک تابع کروتین را فراخوانی میکنید، کد آن را اجرا نمیکند. در عوض، یک شیء کروتین برمیگرداند. این شیء یک طرح کلی برای محاسبه است، اما تا زمانی که توسط یک حلقه رویداد هدایت نشود، هیچ اتفاقی نمیافتد.
مثال:
async def fetch_data(url): ...
فراخوانی fetch_data("http://example.com")
یک شیء کروتین به شما میدهد. تا زمانی که آن را await
نکنید یا آن را به عنوان یک Task برنامهریزی نکنید، بیاثر است.
2. وظایف
یک asyncio.Task
چیزی است که شما برای برنامهریزی یک کروتین برای اجرا بهطور همزمان روی حلقه رویداد استفاده میکنید. شما یک Task را با استفاده از asyncio.create_task(my_coroutine())
ایجاد میکنید. یک Task کروتین شما را بستهبندی میکند و بلافاصله آن را برنامهریزی میکند تا به محض اینکه حلقه رویداد فرصتی پیدا کرد، «در پسزمینه» اجرا شود. نکتهی مهمی که باید در اینجا درک کنید این است که یک Task زیرمجموعهای از Future است. این یک Future تخصصی است که میداند چگونه یک کروتین را هدایت کند.
هنگامی که کروتین بستهبندی شده تکمیل میشود و مقداری را برمیگرداند، Task (که به یاد دارید، یک Future است) به طور خودکار نتیجه خود را تنظیم میکند. اگر کروتین یک استثنا را ایجاد کند، استثنای Task تنظیم میشود.
3. آیندهها
یک asyncio.Future
ساده حتی اساسیتر است. بر خلاف یک Task، به هیچ کروتین خاصی متصل نیست. این فقط یک مکاننما خالی است. چیز دیگری—بخش دیگری از کد شما، یک کتابخانه، یا خود حلقه رویداد—مسئول تنظیم صریح نتیجه یا استثنای آن در زمان بعدی است. وظایف این فرآیند را به طور خودکار برای شما مدیریت میکنند، اما با یک Future خام، مدیریت به صورت دستی است.
در اینجا یک جدول خلاصه برای روشنتر کردن تمایز آمده است:
مفهوم | چیزی که هست | چگونه ایجاد میشود | مورد استفاده اصلی |
---|---|---|---|
کروتین | یک تابع تعریف شده با async def ؛ یک طرح کلی محاسباتی مبتنی بر ژنراتور. |
async def my_func(): ... |
تعریف منطق ناهمزمان. |
Task | یک زیر کلاس Future که یک کروتین را بستهبندی کرده و روی حلقه رویداد اجرا میکند. | asyncio.create_task(my_func()) |
اجرای کروتینها بهطور همزمان («آتش و فراموشی»). |
Future | یک شیء awaitable سطح پایین که یک نتیجهی احتمالی را نشان میدهد. | loop.create_future() |
تعامل با کد مبتنی بر callback؛ همگامسازی سفارشی. |
به طور خلاصه: شما کروتینها را مینویسید. شما آنها را با استفاده از Tasks بهطور همزمان اجرا میکنید. هر دو Tasks و عملیات I/O زیربنایی از آیندهها به عنوان مکانیزم اساسی برای علامتدهی به تکمیل استفاده میکنند.
چرخهی عمر یک Future
یک Future از طریق مجموعهای ساده اما مهم از حالتها انتقال مییابد. درک این چرخهی عمر برای استفادهی مؤثر از آنها کلیدی است.
حالت 1: Pending
وقتی یک Future برای اولین بار ایجاد میشود، در حالت pending قرار دارد. هیچ نتیجه و هیچ استثنایی ندارد. منتظر است که کسی آن را کامل کند.
import asyncio
async def main():
# Get the current event loop
loop = asyncio.get_running_loop()
# Create a new Future
my_future = loop.create_future()
print(f"Is the future done? {my_future.done()}") # Output: False
# To run the main coroutine
asyncio.run(main())
حالت 2: Finishing (تنظیم یک نتیجه یا استثنا)
یک Future در حالت pending را میتوان به یکی از دو روش تکمیل کرد. این کار معمولاً توسط «تولیدکنندهی» نتیجه انجام میشود.
1. تنظیم یک نتیجهی موفق با set_result()
:
هنگامی که عملیات ناهمزمان با موفقیت تکمیل میشود، نتیجهی آن با استفاده از این روش به Future متصل میشود. این Future را به حالت finished تغییر میدهد.
2. تنظیم یک استثنا با set_exception()
:
اگر عملیات شکست بخورد، یک شیء استثنا به Future متصل میشود. این نیز Future را به حالت finished تغییر میدهد. هنگامی که یک کروتین دیگر این Future را await
میکند، استثنای متصل شده افزایش مییابد.
حالت 3: Finished
هنگامی که یک نتیجه یا یک استثنا تنظیم شد، Future done در نظر گرفته میشود. حالت آن اکنون نهایی است و نمیتواند تغییر کند. شما میتوانید این را با روش future.done()
بررسی کنید. هر کروتینی که منتظر این Future بود، اکنون از خواب بیدار میشود و اجرای خود را از سر میگیرد.
(اختیاری) حالت 4: Cancelled
یک Future در حالت pending را میتوان با فراخوانی متد future.cancel()
نیز لغو کرد. این یک درخواست برای رها کردن عملیات است. اگر لغو موفقیتآمیز باشد، Future وارد حالت cancelled میشود. وقتی مورد انتظار قرار میگیرد، یک Future لغو شده یک CancelledError
را افزایش میدهد.
کار با آیندهها: مثالهای عملی
تئوری مهم است، اما کد آن را واقعی میکند. بیایید نگاهی بیندازیم به اینکه چگونه میتوانید از آیندههای خام برای حل مشکلات خاص استفاده کنید.
مثال 1: یک سناریوی تولیدکننده/مصرفکننده دستی
این مثال کلاسیکی است که الگوی ارتباطی اصلی را نشان میدهد. ما یک کروتین (`consumer`) خواهیم داشت که منتظر یک Future است، و کروتین دیگری (`producer`) که مقداری کار انجام میدهد و سپس نتیجه را روی آن Future تنظیم میکند.
import asyncio
import time
async def producer(future):
print("Producer: Starting to work on a heavy calculation...")
await asyncio.sleep(2) # Simulate I/O or CPU-intensive work
result = 42
print(f"Producer: Calculation finished. Setting result: {result}")
future.set_result(result)
async def consumer(future):
print("Consumer: Waiting for the result...")
# The 'await' keyword pauses the consumer here until the future is done
result = await future
print(f"Consumer: Got the result! It's {result}")
async def main():
loop = asyncio.get_running_loop()
my_future = loop.create_future()
# Schedule the producer to run in the background
# It will work on completing my_future
asyncio.create_task(producer(my_future))
# The consumer will wait for the producer to finish via the future
await consumer(my_future)
asyncio.run(main())
# Expected Output:
# Consumer: Waiting for the result...
# Producer: Starting to work on a heavy calculation...
# (2-second pause)
# Producer: Calculation finished. Setting result: 42
# Consumer: Got the result! It's 42
در این مثال، Future به عنوان یک نقطهی همگامسازی عمل میکند. `consumer` نمیداند یا اهمیتی نمیدهد که چه کسی نتیجه را ارائه میکند. فقط به خود Future اهمیت میدهد. این کار تولیدکننده و مصرفکننده را از هم جدا میکند که یک الگوی بسیار قدرتمند در سیستمهای همروند است.
مثال 2: پل زدن به APIهای مبتنی بر Callback
این یکی از قدرتمندترین و رایجترین موارد استفاده برای آیندههای خام است. بسیاری از کتابخانههای قدیمیتر (یا کتابخانههایی که باید با C/C++ تعامل داشته باشند) بومی async/await
نیستند. در عوض، آنها از یک سبک مبتنی بر callback استفاده میکنند، که در آن یک تابع برای اجرا پس از تکمیل ارسال میکنید.
آیندهها یک پل عالی برای مدرنسازی این APIها فراهم میکنند. ما میتوانیم یک تابع wrapper ایجاد کنیم که یک Future awaitable را برمیگرداند.
بیایید تصور کنیم که یک تابع فرضی legacy legacy_fetch(url, callback)
داریم که یک URL را واکشی میکند و وقتی کارش تمام شد، callback(data)
را فراخوانی میکند.
import asyncio
from threading import Timer
# --- This is our hypothetical legacy library ---
def legacy_fetch(url, callback):
# This function is not async and uses callbacks.
# We simulate a network delay using a timer from the threading module.
print(f"[Legacy] Fetching {url}... (This is a blocking-style call)")
def on_done():
data = f"Some data from {url}"
callback(data)
# Simulate a 2-second network call
Timer(2, on_done).start()
# -----------------------------------------------
async def modern_fetch(url):
"""Our awaitable wrapper around the legacy function."""
loop = asyncio.get_running_loop()
future = loop.create_future()
def on_fetch_complete(data):
# This callback will be executed in a different thread.
# To safely set the result on the future belonging to the main event loop,
# we use loop.call_soon_threadsafe.
loop.call_soon_threadsafe(future.set_result, data)
# Call the legacy function with our special callback
legacy_fetch(url, on_fetch_complete)
# Await the future, which will be completed by our callback
return await future
async def main():
print("Starting modern fetch...")
data = await modern_fetch("http://example.com")
print(f"Modern fetch complete. Received: '{data}'")
asyncio.run(main())
این الگو فوقالعاده مفید است. تابع `modern_fetch` تمام پیچیدگی callback را پنهان میکند. از دیدگاه `main`، این فقط یک تابع `async` منظم است که میتواند مورد انتظار قرار گیرد. ما با موفقیت یک API قدیمی را «futurized» کردهایم.
توجه: استفاده از loop.call_soon_threadsafe
زمانی که callback توسط یک thread متفاوت اجرا میشود، حیاتی است، همانطور که در عملیات I/O در کتابخانههایی که با asyncio ادغام نشدهاند، رایج است. این تضمین میکند که future.set_result
با خیال راحت در متن حلقه رویداد asyncio فراخوانی میشود.
چه زمانی از Futureهای خام استفاده کنیم (و چه زمانی نه)
با در دسترس بودن انتزاعهای سطح بالای قدرتمند، مهم است که بدانید چه زمانی باید به ابزاری سطح پایین مانند Future دسترسی پیدا کنید.
از Futureهای خام زمانی استفاده کنید که:
- تعامل با کد مبتنی بر callback: همانطور که در مثال بالا نشان داده شد، این مورد استفاده اصلی است. آیندهها پل ایدهآل هستند.
- ساخت پریمیتوهای همگامسازی سفارشی: اگر نیاز به ایجاد نسخهی خود از یک Event، Lock یا Queue با رفتارهای خاص دارید، آیندهها مولفهی اصلی هستند که بر روی آن ساختهاید.
- یک نتیجه توسط چیزی غیر از یک کروتین تولید میشود: اگر یک نتیجه توسط یک منبع رویداد خارجی (به عنوان مثال، سیگنالی از یک فرآیند دیگر، یک پیام از یک کلاینت websocket) تولید میشود، یک Future روشی عالی برای نشان دادن آن رویداد در حال انتظار در دنیای asyncio است.
از Futureهای خام اجتناب کنید (به جای آن از Tasks استفاده کنید) زمانی که:
- فقط میخواهید یک کروتین را بهطور همزمان اجرا کنید: این کار
asyncio.create_task()
است. این کار بستهبندی کروتین، زمانبندی آن و انتشار نتیجه یا استثنای آن به Task (که یک Future است) را انجام میدهد. استفاده از یک Future خام در اینجا، دوبارهسازی چرخ است. - مدیریت گروههایی از عملیات همزمان: برای اجرای چندین کروتین و انتظار برای تکمیل آنها، APIهای سطح بالا مانند
asyncio.gather()
،asyncio.wait()
وasyncio.as_completed()
بسیار ایمنتر، خواناتر و کمتر مستعد خطا هستند. این توابع مستقیماً روی کروتینها و Tasks کار میکنند.
مفاهیم و اشکالات پیشرفته
آیندهها و حلقه رویداد
یک Future ذاتاً به حلقه رویدادی که در آن ایجاد شده است مرتبط است. یک عبارت await future
کار میکند زیرا حلقه رویداد در مورد این Future خاص میداند. این درک میکند که وقتی یک await
را روی یک Future در حال انتظار میبیند، باید اجرای کروتین فعلی را به حالت تعلیق درآورد و به دنبال کارهای دیگری باشد. وقتی Future در نهایت تکمیل شد، حلقه رویداد میداند کدام کروتین به حالت تعلیق درآمده را بیدار کند.
به همین دلیل است که همیشه باید یک Future را با استفاده از loop.create_future()
ایجاد کنید، جایی که loop
حلقه رویداد در حال اجرا است. تلاش برای ایجاد و استفاده از آیندهها در سراسر حلقههای رویداد مختلف (یا threadهای مختلف بدون همگامسازی مناسب) منجر به خطا و رفتار غیرقابل پیشبینی خواهد شد.
await
واقعاً چه کاری انجام میدهد
وقتی مفسر پایتون با result = await my_future
مواجه میشود، چند مرحله را در پشت صحنه انجام میدهد:
my_future.__await__()
را فراخوانی میکند، که یک iterator برمیگرداند.- بررسی میکند که آیا future قبلاً انجام شده است یا خیر. اگر چنین است، نتیجه را دریافت میکند (یا استثنا را ایجاد میکند) و بدون تعلیق ادامه میدهد.
- اگر future در حال انتظار است، به حلقه رویداد میگوید: «اجرای من را به حالت تعلیق درآورید و لطفاً وقتی این future خاص کامل شد، من را بیدار کنید.»
- سپس حلقه رویداد بر عهده میگیرد و وظایف آمادهی دیگر را اجرا میکند.
- هنگامی که
my_future.set_result()
یاmy_future.set_exception()
فراخوانی میشود، حلقه رویداد Future را به عنوان done علامتگذاری میکند و کروتین به حالت تعلیق درآمده را برنامهریزی میکند تا در تکرار بعدی حلقه از سر گرفته شود.
اشکال رایج: اشتباه گرفتن Future با Tasks
یک اشتباه رایج، تلاش برای مدیریت اجرای یک کروتین بهصورت دستی با یک Future است، در حالی که Task ابزار مناسبی است.
راه اشتباه (بیش از حد پیچیده):
# This is verbose and unnecessary
async def main_wrong():
loop = asyncio.get_running_loop()
future = loop.create_future()
# A separate coroutine to run our target and set the future
async def runner():
try:
result = await some_other_coro()
future.set_result(result)
except Exception as e:
future.set_exception(e)
# We have to manually schedule this runner coroutine
asyncio.create_task(runner())
# Finally, we can await our future
final_result = await future
راه درست (با استفاده از یک Task):
# A Task does all of the above for you!
async def main_right():
# A Task is a Future that automatically drives a coroutine
task = asyncio.create_task(some_other_coro())
# We can await the task directly
final_result = await task
از آنجایی که Task
یک زیر کلاس از Future
است، مثال دوم نه تنها تمیزتر است، بلکه از نظر عملکرد معادل و کارآمدتر نیز هست.
نتیجهگیری: بنیان Asyncio
آیندهی Asyncio قهرمان گمنام اکوسیستم ناهمزمان پایتون است. این پریمیتو سطح پایین است که جادوی سطح بالای async/await
را ممکن میسازد. در حالی که کدنویسی روزانهی شما عمدتاً شامل نوشتن کروتینها و برنامهریزی آنها به عنوان Tasks میشود، درک آیندهها بینش عمیقی در مورد چگونگی اتصال همه چیز به شما میدهد.
با تسلط بر آیندهها، شما توانایی موارد زیر را به دست میآورید:
- با اطمینان اشکالزدایی کنید: وقتی یک
CancelledError
یا یک کروتین را میبینید که هرگز برنمیگردد، حالت Future یا Task زیربنایی را درک خواهید کرد. - ادغام هر کدی: اکنون شما این قدرت را دارید که هر API مبتنی بر callback را بستهبندی کنید و آن را به یک شهروند درجه یک در دنیای async مدرن تبدیل کنید.
- ساخت ابزارهای پیچیده: دانش آیندهها اولین قدم برای ایجاد سازههای برنامهنویسی همزمان و موازی پیشرفتهی خودتان است.
بنابراین، دفعهی بعد که از asyncio.create_task()
یا await asyncio.gather()
استفاده میکنید، لحظهای را صرف قدردانی از Future فروتن کنید که بیوقفه در پشت صحنه کار میکند. این پایه و اساس محکمی است که برنامههای ناهمزمان پایتون قوی، مقیاسپذیر و ظریف بر روی آن ساخته شدهاند.